package com.hube.platform.tenant.configuration;

import com.codahale.metrics.MetricRegistry;
import com.hube.platform.tenant.constant.StringPool;
import com.hube.platform.tenant.exception.ApplicationException;
import com.hube.platform.tenant.exception.ServerError;
import com.hube.platform.tenant.helper.DynamicDataSourceContextHolder;
import com.hube.platform.tenant.helper.HikariCPHelper;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * 切换数据源必须在调用service之前进行，也就是开启事务之前
 *
 * @author : Aaron
 * @date : 2022/6/2
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);

    private static Map<Object, Object> targetDataSources = new HashMap<>();


    /**
     * 如果不希望数据源在启动配置时就加载好，可以定制这个方法，从任何你希望的地方读取并返回数据源
     * 比如从数据库、文件、外部接口等读取数据源信息，并最终返回一个DataSource实现类对象即可
     */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
     * 如果希望所有数据源在启动配置时就加载好，这里通过设置数据源Key值来切换数据，定制这个方法
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }

    /**
     * 设置默认数据源
     *
     * @param defaultDataSource
     */
    @Override
    public void setDefaultTargetDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }

    /**
     * 设置数据源
     *
     * @param dataSources
     */
    @Override
    public void setTargetDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        this.targetDataSources = dataSources; //为了可以动态添加数据源
        // 将数据源的 key 放到数据源上下文的 key 集合中，用于切换时判断数据源是否存在
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }

    /**
     * 动态增加数据源
     *
     * @param dbConfig 数据源属性
     * @param tenantId 租户唯一标识
     * @param dbName   数据库名
     * @return
     */
    public synchronized HikariDataSource addDataSource(String tenantId, String dbName, HikariConfig dbConfig) {
        HikariDataSource dataSource;
        try {
            //测试数据库配置是否正确
            testDatabaseConnect(tenantId, dbName, dbConfig);
            if (DynamicDataSource.isExistDataSource(tenantId)) {
                return (HikariDataSource) targetDataSources.get(tenantId);
            }
            dbConfig.setPoolName(HikariCPHelper.generatePoolName(tenantId));
            dataSource = new HikariDataSource(dbConfig);
            //注册监控信息
            dataSource.setMetricRegistry(new MetricRegistry());
            targetDataSources.put(tenantId, dataSource);
            super.afterPropertiesSet();
            //维护全局对象
            DynamicDataSourceContextHolder.addDataSourceKeys(Arrays.asList(tenantId));
            log.info("----dataSource {} has been added----", tenantId);
        } catch (Exception e) {
            log.error("dataSource add error :{} ,{}", dbConfig, e);
            throw new ApplicationException(ServerError.DATASOURCE_CREATE_ERROR);
        }
        return dataSource;
    }

    /**
     * 动态更新数据源
     *
     * @param dbConfig 数据源属性
     * @param tenantId 租户唯一标识
     * @param dbName   数据库名
     * @return
     */
    public synchronized HikariDataSource updateDataSource(String tenantId, String dbName, HikariConfig dbConfig) {
        HikariDataSource dataSource;
        try {
            testDatabaseConnect(tenantId, dbName, dbConfig);
            //更新
            dbConfig.setPoolName(HikariCPHelper.generatePoolName(tenantId));
            dataSource = new HikariDataSource(dbConfig);
            //注册监控信息
            dataSource.setMetricRegistry(new MetricRegistry());
            targetDataSources.put(tenantId, dataSource);
            super.afterPropertiesSet();
            log.info("----dataSource {} has been added----", tenantId);
            //维护全局对象
            DynamicDataSourceContextHolder.addDataSourceKeys(Arrays.asList(tenantId));
        } catch (Exception e) {
            log.error("dataSource add error :{} ,{}", dbConfig, e);
            throw new ApplicationException(ServerError.DATASOURCE_CREATE_ERROR);
        }
        return dataSource;
    }

    /**
     * 删除数据源
     *
     * @param key
     * @return
     */
    public synchronized boolean deleteDataSource(String key) {
        if (isExistDataSource(key)) {
            //关闭
            HikariDataSource targetDs = (HikariDataSource) targetDataSources.get(key);
            targetDs.close();
            //移除
            targetDataSources.remove(key);
            super.afterPropertiesSet();
            log.info("----dataSource {} has been deleted----", key);
            DynamicDataSourceContextHolder.removeDataSourceKey(key);
            return true;
        } else {
            return false;
        }
    }

    /**
     * 移除所有数据源
     *
     * @return
     */
    public synchronized boolean clearDataSource() {
        //移除
        targetDataSources.entrySet().clear();
        super.afterPropertiesSet();
        log.info("----all dataSources deleted----");
        return true;
    }

    /**
     * 是否存在当前key的 DataSource
     *
     * @param key
     * @return 存在返回 true, 不存在返回 false
     */
    public static boolean isExistDataSource(String key) {
        return targetDataSources.containsKey(key);
    }


    /**
     * 根据jdbc_url,去除DB name，获取数据库连接URL，进行预连接
     *
     * @param url
     * @return
     */
    private String getPreparedConnectUrl(String url) {
        URI uri = null;
        try {
            uri = new URI(url.replace("jdbc:", StringPool.BLANK));
        } catch (URISyntaxException e) {
            log.error("----url解析异常----：{}，{}", url, e);
            throw new ApplicationException(ServerError.URL_RESOLVE_ERROR);
        }
        String host = uri.getHost();
        int port = uri.getPort();
        return "jdbc:mysql://" + host + StringPool.COLON + port;
    }

    /**
     * 测试数据库URL配置是否正确
     *
     * @param tenantId
     * @param dbName
     * @param dbConfig
     */
    private void testDatabaseConnect(String tenantId, String dbName, HikariConfig dbConfig) throws SQLException {
        //初始化连接一次，排除配置数据库异常
        Connection connection = null;
        try {
            Class.forName(dbConfig.getDriverClassName());
            connection = DriverManager.getConnection(getPreparedConnectUrl(dbConfig.getJdbcUrl()), dbConfig.getUsername(), dbConfig.getPassword());
            Statement statement = connection.createStatement();
            // 如果不存在，则动态创建数据库
            statement.executeUpdate("create database if not exists `" + dbName + "` DEFAULT CHARACTER SET = `utf8` COLLATE `utf8_general_ci`;");
            statement.close();
            log.info("----database connect test----{}，{}", tenantId, connection.isClosed());
        } catch (Exception e) {
            log.error("database configure error:{},{}", tenantId, e);
            throw new ApplicationException(ServerError.DB_CONFIGURE_ERROR);
        } finally {
            if (connection != null && !connection.isClosed()) {
                connection.close();
            }
        }
    }

    /**
     * 获取当前所有的数据源配置
     *
     * @return
     */
    public static Map<Object, Object> getTargetDataSources() {
        return targetDataSources;
    }
}